/* @flow */

// 创建一个空的冻结对象 emptyObject (不可扩展、不可配置、不可写)
// Object.freeze: 用于冻结一个对象，使对象不可更改
export const emptyObject = Object.freeze({})

// 判断给定变量是否是未定义，当变量值为 null时，也会认为其是未定义
export function isUndef (v: any): boolean %checks {
  return v === undefined || v === null
}

// 判断给定变量是否是定义，当变量值为 null时，也会认为其是未定义
export function isDef (v: any): boolean %checks {
  return v !== undefined && v !== null
}

// 判断给定变量值是否为 true
export function isTrue (v: any): boolean %checks {
  return v === true
}

// 判断给定变量值是否为 false
export function isFalse (v: any): boolean %checks {
  return v === false
}

/**
 * 判断给定变量是否是原始类型值，即：string、number、boolean以及 symbol
 */
export function isPrimitive (value: any): boolean %checks {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

/**
 * 判定一个非null的obj 是不是一个对象。typeof(null,[],{})的结果均为object
 */
export function isObject (obj: mixed): boolean %checks {
  return obj !== null && typeof obj === 'object'
}

/**
 * 在这里我说call方法和apply方法这是很常用的，这里是借用对象中的toString方法
 * call(obj,vaule);obj是一个对象，或方法，把obj的this指向obj,value是参数;就是给个toString方法传递参数
 * apply()与call几乎相同，不同的是call是一个参数，apply后面可以把很多个参数传过去apply(obj,val1,val2,val3…………);
 */
const _toString = Object.prototype.toString

/**
 * 返回给定变量的原始类型字符串
 * 使用 Object.prototype.toString 获取诸如这样的字符串：[object Array]
 * 然后使用 slice 方法截取，最终结果类似于 Array 
 */
export function toRawType (value: any): string {
  // 从第8位开始截取，-1表示截取到最后一位
  return _toString.call(value).slice(8, -1)
}

/**
 * 判断给定变量是否是纯对象
 */
export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

/**
 * 判断给定变量是否是正则对象
 */
export function isRegExp (v: any): boolean {
  return _toString.call(v) === '[object RegExp]'
}

/**
 * 判断给定变量的值是否是有效的数组索引。如果是有效的则返回 true，否则返回 false
 * 一个有效的数组索引要满足两个条件：1、大于等于 0 的整数，2、在条件一的基础上，这个整数不能是无限的
 * isFinite(val) 保证了该值是有限的
 */
export function isValidArrayIndex (val: any): boolean {
  const n = parseFloat(String(val))
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

/**
 * 将给定变量的值转换为 string 类型并返回
 * 当变量值为 null 时，返回空字符串
 * 当值的类型为 object 返回 JSON.stringify(val, null, 2)  缩进2个空格
 * 否则返回 String(val)
 */
export function toString (val: any): string {
  return val == null
    ? ''
    : typeof val === 'object'
      ? JSON.stringify(val, null, 2)
      : String(val)
}

/**
 * 将给定 string 类型的值转换为 number 类型并返回。如果转换失败，返回初始值
 * isNaN() 函数通常用于检测 parseFloat() 和 parseInt() 的结果，以判断它们表示的是否是合法的数字
 */
export function toNumber (val: string): number | string {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

/**
 * str 一个以逗号分隔的字符串
 * expectsLowerCase 是否小写
 * 将字符串转换成map对象。
 */
export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  // 遍历 list 并以 list 中的元素作为 map 的 key，将其设置为 true
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

/**
 * 检查是否是内置的标签：实际上slot和component为Vue的内置函数。
 */
export const isBuiltInTag = makeMap('slot,component', true)

/**
 * 检查给定字符串是否是内置的属性(key、ref、slot、slot-scope 以及is等属性皆属于内置属性，我们不能使用这些属性作为 props 的名字)
 */
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')

/**
 * 从数组中移除指定元素（只能移除简单元素，无法对复杂的数组元素进行处理，且indexOf为ES7语法）
 */
export function remove (arr: Array<any>, item: any): Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

/**
 * 检查对象 obj 是否具有属性值key
 */
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  return hasOwnProperty.call(obj, key)
}

/**
 * 为一个纯函数(输入不变则输出不变)创建一个缓存版本的函数，返回值： 新的函数
 * 应用场景： 将aaa-bbb之类的字符串转换成驼峰写法。只需转译一次并将结果缓存，当再次需要转译该相同的字符串时，我们只需要从缓存中读取即可
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  // 该函数与原函数 fn 的区别就在于：先读取缓存
  return (function cachedFn (str: string) {
    const hit = cache[str]
    // 如果有命中则直接返回缓存的值，否则采用原函数 fn 计算一次并且设置缓存，然后返回结果
    return hit || (cache[str] = fn(str))
  }: any)
}

/**
 * 连字符转驼峰：用正则来全局匹配字符串中 中横线及连字符后的一个字符
 * 如：camelize('aaa-bbb')  // aaaBbb
 */
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
  // 如果连字符后有字符，则将匹配到的内容使用该字符的大写形式替换，否则使用空字符串替换
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

/**
 * 首字母大写。这是一个由 cached 函数生成的新函数
 */
export const capitalize = cached((str: string): string => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})

/**
 * 驼峰转连字符
 * 用来全局匹配字符串中的大写字母，并且该大写字母前必须不是单词的边界。
 * 一个单词两边只要没有数字、字母、汉字、下划线就可以认为是独立单词，而\B用来表示单词的分界处(即前或后必有一个是字母)，而且是就近匹配的。
 * /\B([A-Z])/g  表示大写字母前紧是的有其它词存在的，不会匹配大写字母开头的独立单词
 */
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
  // 将匹配的内容使用连字符和捕获组的字符替换，最后转为小写，-$1表示将$1位置的内容替换为-
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})

/**
 * Simple bind polyfill for environments that do not support it,
 * e.g., PhantomJS 1.x. Technically, we don't need this anymore
 * since native bind is now performant enough in most browsers.
 * But removing it would mean breaking code that was able to run in
 * PhantomJS 1.x, so this must be kept for backward compatibility.
 */

/* 每次加元素计算数组长度 */
function polyfillBind (fn: Function, ctx: Object): Function {
  function boundFn (a) {
    // 取参数的长度
    const l = arguments.length
    // 双重三目运算符写法
    return l
      ? l > 1
        ? fn.apply(ctx, arguments) // 对多个参数数组apply，返回总长度
        : fn.call(ctx, a) // 一个参数就直接call，返回总长度
      : fn.call(ctx) // 没有新参数，就返回原长度
  }

  boundFn._length = fn.length
  return boundFn
}

function nativeBind (fn: Function, ctx: Object): Function {
  return fn.bind(ctx)
}

export const bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind

/**
 * 将类数组对象转换为数组
 */
export function toArray (list: any, start?: number): Array<any> {
  start = start || 0
  let i = list.length - start
  const ret: Array<any> = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

/**
 * 将 _from 对象的属性混合到 to 对象中
 * 如果_from和to对象中存在相当的key值 ，则_from的属性会覆盖to的属性(后面会覆盖前面的属性) 
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

/**
 * 将一个对象数组合并到一个对象中，并返回该对象
 */
export function toObject (arr: Array<any>): Object {
  const res = {}
  for (let i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i])
    }
  }
  return res
}

/* eslint-disable no-unused-vars */

/**
 * 空函数，什么都不做，用于初始化一些值为函数的变量
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
export function noop (a?: any, b?: any, c?: any) {}

/**
 * 始终返回 false 的函数
 */
export const no = (a?: any, b?: any, c?: any) => false

/* eslint-enable no-unused-vars */

/**
 * 一个输入和返回值一样的纯函数
 */
export const identity = (_: any) => _

/**
 * 根据编译器(compiler)的 modules 生成一个静态键字符串（简单来说就是通过对 modules 数组的遍历，将所有的 staticKeys 收集到
 * 一个数组，最终转换成一个以逗号 , 拼接的字符串）
 * modules格式如下：
 * [{staticKeys: ['staticClass'], transformNode, genData},{staticKeys: ['staticStyle'], transformNode, genData}]
 */
export function genStaticKeys (modules: Array<ModuleOptions>): string {
  return modules.reduce((keys, m) => {
    return keys.concat(m.staticKeys || [])
  }, []).join(',')
}

/**
 * 检查两个变量a,b是否相等(完全相等)
 * 
 */
export function looseEqual (a: any, b: any): boolean {
  // 1、将a,b均为基本类型时，直接使用===进行判定
  if (a === b) return true
  // 2、判定两个变量是不是均为非null的Object类型
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  // 如果均为非null的Object类型(实际上，typeof(null,[],{})的结果均为object)
  if (isObjectA && isObjectB) {
    // Array.isArray()为ES5所添加，可以用来判断对象是否为数组对象，通过try可防止浏览器不支持时的异常
    try {
      const isArrayA = Array.isArray(a)
      const isArrayB = Array.isArray(b)
      // 2.1 再次进行判断。如果a、b均为数组时且length相等的情况下，对数组的每一个元素再次进行递归判断
      if (isArrayA && isArrayB) {
        // 仅当两个数组完全一样时才返回true，只要有一项不一样就会返回false
        return a.length === b.length && a.every((e, i) => {
          return looseEqual(e, b[i])
        })
      // 2.2  如果a,b均为时间类型，则直接获取毫秒数进行对比
      } else if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime()
      // 2.3 如果a,b均不是数组，则即a,b对象的key集合来比较长度，如果长度相等，则即对应的元素进行递归判断
      } else if (!isArrayA && !isArrayB) {
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)
        return keysA.length === keysB.length && keysA.every(key => {
          return looseEqual(a[key], b[key])
        })
      // 2.4 如果a、b中一个是数组，一个是对象，直接返回 false 
      } else {
        return false
      }
    } catch (e) {
      return false
    }
  // 3、如果a、b均不为 object 类型的值，对两个值调用 String() 方法进行比较
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  // 4、如果a、b有一个不是Object类型，则直接返回false
  } else {
    return false
  }
}

/**
 * 返回 val 在 arr 中的索引
 * 可用来判定复杂的数组中某一元素在数组中的位置
 */
export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
  for (let i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) return i
  }
  return -1
}

/**
 * 只调用一次的函数：通过标识让函数只生效一次
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
